package toolib.geom;

/**
 * 
 * 
 * @author Steffen Fiedler
 */
public class Spline2D {
	
	public static final String	BEZIER				= "Bezier";
	public static final String	CUBIC				= "Cubic";
	public static final String	LAGRANGE			= "Lagrange";
	
	public boolean				autoGenerate		= true;
	public boolean				autoResolution		= true;
	public int					autoResolutionStep	= 6;
	
	private String				_type				= BEZIER;
	
	private V2fList				_points;
	private V2f[]				_vertices;
	private int					_resolution			= 40;
	
	private V2f[]				_tmpBezier			= null;
	
	
	public Spline2D () {
		_points		= new V2fList ();
		_vertices	= new V2f[_resolution];
	}
	
	public Spline2D (V2f[] theControlPoints) {
		_points		= new V2fList ();
		_vertices	= new V2f[_resolution];
		points (theControlPoints);
	}
	
	public Spline2D (V2f[] theControlPoints, String theType) {
		if (theType != BEZIER && 
			theType != CUBIC && 
			theType != LAGRANGE) {
				
			throw new IllegalArgumentException ("Unknown spline type.");
		}
		_points		= new V2fList ();
		_vertices	= new V2f[_resolution];
		_type		= theType;
		points (theControlPoints);
	}
	
	public Spline2D (V2fList theControlPoints) {
		_points		= theControlPoints;
		_vertices	= new V2f[_resolution];
	}
	
	public Spline2D (V2fList theControlPoints, String theType) {
		if (theType != BEZIER && 
			theType != CUBIC && 
			theType != LAGRANGE) {
			
			throw new IllegalArgumentException ("Unknown spline type.");
		}
		_points		= theControlPoints;
		_type		= theType;
		_vertices	= new V2f[_resolution];
	}
	
	/**
	 * Adds a point to the control path using x and y 
	 * position coordinates and generates the <code>
	 * vertices</code> if <code>autoGenerate</code> is 
	 * <code>true</code>.
	 * 
	 * @param theX Point x-coordinate.
	 * @param theY Point y-coordinate.
	 */
	public final void addPoint (float theX, float theY) {
		_points.add (new V2f (theX, theY));
		if (autoGenerate) {
			generate ();
		}
	}
	
	/**
	 * Adds a point to the control path by using a vector 
	 * and generates the <code>vertices</code> if <code>
	 * autoGenerate</code> is <code>true</code>.
	 * 
	 * @param thePoint The vector to add.
	 */
	public final void addPoint (V2f thePoint) {
		addPoint (thePoint.x, thePoint.y);
	}
	
	/**
	 * Adds a list of points to the end of the control path 
	 * using vectors and generates the <code>vertices</code> 
	 * if <code>autoGenerate</code> is <code>true</code>.
	 * 
	 * @param thePoints List of points to add.
	 */
	public final void addPoint (V2f[] thePoints) {
		for (V2f o : thePoints) {
			_points.add (o);
		}
		if (autoGenerate) {
			generate ();
		}
	}
	
	/**
	 * Removes all points from the control path and updates 
	 * the vertices list if <code>autoGenerate</code> is 
	 * <code>true</code>.
	 */
	public final void clearPoints () {
		_points.clear ();
		if (autoGenerate) {
			generate ();
		}
	}
	
	/**
	 * Control path segment as two dimensional line for 
	 * a given section index.
	 * 
	 * @param theIndex 
	 * @return
	 */
	public final Line2D controlPathSegment (int theIndex) {
		return new Line2D (	_points.get (theIndex).x, 
							_points.get (theIndex).y, 
							_points.get (theIndex + 1).x, 
							_points.get (theIndex + 1).y);
	}
	
	public final V2f[] generate () {
		return generate (0, 1f);
	}
	
	public final V2f[] generate (float theStart, float theEnd) {
		
		if (_points.size () < 2) {
			_vertices = new V2f[0];
			return null;
		}
		
		if (autoResolution) {
			if (autoResolutionStep < 3) {
				throw new RuntimeException ("The minimum for autoResolutionStep must be 3.");
			}
			_resolution = autoResolutionStep * _points.size ();
			_vertices	= new V2f[_resolution];
		}
		
		float t = theStart;
		
		if (_type == BEZIER) {
			if (_tmpBezier == null) {
				_tmpBezier = new V2f[_points.size ()];
			}
			else if (_tmpBezier.length != _points.size ()){
				_tmpBezier = new V2f[_points.size ()];
			}
			
			for (int r=0; r < _resolution; r++) {
				for (int k=0; k < _points.size (); k++) {
					_tmpBezier[k] = _points.get (k).clone ();
				}
				for (int j=_points.size () - 1; j > 0; j--) {	
					for (int i=0; i < j; i++) {
						_tmpBezier[i].x	= (1 - t) * _tmpBezier[i].x + t * _tmpBezier[i + 1].x;
						_tmpBezier[i].y	= (1 - t) * _tmpBezier[i].y + t * _tmpBezier[i + 1].y;
					}
				}
				_vertices[r] = new V2f (_tmpBezier[0].x, _tmpBezier[0].y);
				t += theEnd / (_resolution - theEnd);
			}
		}else if (_type == CUBIC) {
			
			// TODO: Fix problems when autoResolution is off
			
			float[] gamma		= new float[_points.size ()];
			V2f[] delta			= new V2f[_points.size ()];
			V2f[] d				= new V2f[_points.size ()];
			
			float cx;
			float cy;
			float dx;
			float dy;
			float u;
			
			int index;
			int step;
			
			final int num = _points.size () - 1;
			
			for (int i=0; i <= num; i++) {
				if (i == 0) {
					gamma[i] = .5f;
					delta[0] = new V2f (3 * (_points.get (1).x - _points.get (0).x) * .5f, 
										3 * (_points.get (1).y - _points.get (0).y) * .5f);
					
				}else if (i == num){
					gamma[i] = 1f / (2 - gamma[num - 1]);
					delta[i] = new V2f (3 * (_points.get (num).x - _points.get (num - 1).x) - delta[num - 1].x * gamma[num], 
										3 * (_points.get (num).y - _points.get (num - 1).y) - delta[num - 1].y * gamma[num]);
				}else{
					gamma[i] = 1f / (4 - gamma[i - 1]);
					delta[i] = new V2f(	(3 * (_points.get (i + 1).x - _points.get (i - 1).x) - delta[i-1].x) * gamma[i], 
										(3 * (_points.get (i + 1).y - _points.get (i - 1).y) - delta[i-1].y) * gamma[i]);
				}
			}
			
			d[num] = delta[num];
			for (int i = num - 1; i >= 0; i--) {
				d[i] = new V2f (delta[i].x - gamma[i] * d[i + 1].x, 
								delta[i].y - gamma[i] * d[i + 1].y);
			}
			
			index	= 0;
			step	= (int) Math.floor (_resolution / (_points.size () - 1)) - 1;
			
			for (int i=0; i < _points.size () - 1; i++) {
				// Cubic polynomial
				cx = 3 * (_points.get (i + 1).x - _points.get (i).x) - 2 * d[i].x - d[i + 1].x;
				cy = 3 * (_points.get (i + 1).y - _points.get (i).y) - 2 * d[i].y - d[i + 1].y;
				dx = 2 * (_points.get (i).x - _points.get (i + 1).x) + d[i].x + d[i + 1].x;
				dy = 2 * (_points.get (i).y - _points.get (i + 1).y) + d[i].y + d[i + 1].y;
				
				if (i == _points.size () - 2) {
					step = _vertices.length - index - 1;
				}
				
				for (int r=0; r <= step; r++) {
					u = r / (float) step;
					_vertices[index]  = new V2f (	(((dx * u) + cx) * u + d[i].x) * u + _points.get (i).x, 
													(((dy * u) + cy) * u + d[i].y) * u + _points.get (i).y);
					index++;
				}
			}
		}else if (_type == LAGRANGE) {
			
			float p;
			float x;
			float y;
			float[] ti = new float[_points.size ()];
			
			for (int i=0; i < ti.length; i++) {
				ti[i] = (1.0f / (ti.length - 1)) * i;
			}
			for (int r = 0; r < _resolution; r++) {
				x = 0;
				y = 0;
				for (int j = 0; j < _points.size (); j++) {
					p = 1;
					for (int i = 0; i < _points.size (); i++) {
						if (i != j) {
							p = p * (t - ti[i]) / (ti[j] - ti[i]);
						}
					}
					x += _points.get (j).x * p;
					y += _points.get (j).y * p;
				}
				_vertices[r] = new V2f (x, y);
				t += 1.0f / (_resolution - 1);
			}
		}
		return _vertices;
	}
	
	/**
	 * Sets the control points that describe the spline 
	 * curve. If the boolean <code>autoGenerate</code> 
	 * is <code>true</code> - the actual curve will be 
	 * generated automatically.
	 * 
	 * @param theControlPoints Array of vector instances.
	 */
	public final void points (V2f[] theControlPoints) {
		_points.clear ();
		for (V2f o : theControlPoints) {
			_points.add (o);
		}
		if (autoGenerate) {
			generate ();
		}
	}
	
	/**
	 * Sets the control points that describe the spline 
	 * curve. If the boolean <code>autoGenerate</code> 
	 * is <code>true</code> - the actual curve will be 
	 * generated automatically.
	 * 
	 * @param theControlPoints 
	 */
	public final void points (V2fList theControlPoints) {
		_points = theControlPoints;
		if (autoGenerate) {
			generate ();
		}
	}
	
	/**
	 * Control path points describing the spline curve.
	 * 
	 * @return ArrayList of two dimensional vectors.
	 */
	public final V2fList points () {
		return _points;
	}
	
	public final int resolution () {
		return _resolution;
	}
	
	/**
	 * Sets the detail level of the spline curve.
	 * @param theResolution
	 */
	public final void resolution (int theResolution) {
		if (theResolution < _points.size ()) {
			return;
		}
		_resolution = theResolution;
		_vertices	= new V2f[_resolution];
		if (autoGenerate) {
			generate ();
		}
	}
	
	public final V2f positionAt (float theProgress) {
		if (theProgress < 0 || theProgress > 1) {
			throw new IllegalArgumentException ("Progress parameter must be between 0 and 1.");
		}
		
		if (_type == BEZIER) {
			V2f[] tmp = new V2f[_points.size ()];
			for (int k=0; k < _points.size (); k++) {
				tmp[k] = _points.get (k).clone ();
			}
			for (int j=_points.size () - 1; j > 0; j--) {	
				for (int i=0; i < j; i++) {
					tmp[i].x = (1 - theProgress) * tmp[i].x + theProgress * tmp[i + 1].x;
					tmp[i].y = (1 - theProgress) * tmp[i].y + theProgress * tmp[i + 1].y;
				}
			}
			return new V2f (tmp[0].x, tmp[0].y);
		}else if (_type == LAGRANGE) {
			return new V2f ();
		}
		return null;
	}
	
	public final String type () {
		return _type;
	}
	
	public final void type (String theSplineType) {
		if (_type == theSplineType) {
			return;
		}
		if (theSplineType != BEZIER && 
			theSplineType != CUBIC && 
			theSplineType != LAGRANGE) {
			
			throw new IllegalArgumentException ("Unknown spline type.");
		}
		_type = theSplineType;
		if (autoGenerate) {
			generate ();
		}
	}
	
	/**
	 * Spline curve vertex points.
	 * 
	 * @return
	 */
	public final V2f[] vertices () {
		if (_vertices == null) {
			_vertices = new V2f[0];
		}
		return _vertices;
	}
	
	public Spline2D clone () {
		Spline2D s = new Spline2D (_points, _type);
		s.resolution (_resolution);
		s.autoGenerate			= autoGenerate;
		s.autoResolution		= autoResolution;
		s.autoResolutionStep	= autoResolutionStep;
		return s;
	}
}